#include <assert.h>
#include <cassert>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <string.h>
#include <vector>

#include "../headers/Shell.h"
#include "../headers/FieldSpecifications.h"
#include "../headers/OpCodes.h"
#include "../headers/ShellSwitchRequest.h"
#include "../headers/Helpers.h"
#include "../headers/ListShellsRequest.h"

using namespace std;

Shell::Shell(string name, string core) : name(name)
{
	this->initialize_shell(new CPU(), new ExecutionContext());

	if (core != "")
	{
		this->load_cpu_snapshot(core);
	}
}


Shell::Shell(CPU* cpu, ExecutionContext* context, string name) : name(name)
{
	this->initialize_shell(cpu, context);
}


Shell::~Shell(void)
{
	delete this->context;
	delete this->cpu;
}


void Shell::initialize_shell(CPU* const cpu, ExecutionContext* const context)
{
	this->initialize_command_map();

	this->cpu = cpu;
	this->context = context;

	this->state = Suspended;
}


void Shell::breakpoint(vector<string>* args)
{
	bool showHelp = true;
	unsigned int address = 0;
	int status = -1;
	vector<string>::const_iterator item = args->begin();

	switch(args->size())
	{
		case 1:
			if(strcmpci((*item).c_str(), "list") == 0)
			{
				this->list_breakpoints(cout);
				showHelp = false;
			}

			break;

		case 2:
			status = xtoi((*(item + 1)).c_str(), &address);
			if(status == 0)
			{
				if(address < CPU::MEMORY_SIZE)
				{
					if(strcmpci((*item).c_str(), "set") == 0)
					{
						this->context->get_breakpoints()->insert(address);
						showHelp = false;
					}
					else if(strcmpci((*item).c_str(), "remove") == 0)
					{
						this->context->get_breakpoints()->erase(address);
						showHelp = false;
					}
				}
				else
				{
					cout << "ERROR: the breakpoint is outside of the the memory" << endl;
					showHelp = false;
				}
			}

			break;

		default:
			break;
	}

	if(showHelp)
	{
		cout << "usage: breakpoint { list | { set | remove } <address> }" << endl;
		cout << "\tlist:      Lists all breakpoints" << endl;
		cout << "\tset:       Sets a breakpoint on <address>" << endl;
		cout << "\tremove:    Removes the breakpoint from <address>" << endl;
		cout << "\t<address>: A hexadecimal address of the breakpoint" << endl;
	}
}


void Shell::list_breakpoints(ostream& os) const
{
	os << "Breakpoints:" << endl;
	set<unsigned int>* breakpoints = this->context->get_breakpoints();
	for(set<unsigned int>::const_iterator it = breakpoints->begin(); it != breakpoints->end(); it++)
	{
		os << "\t" << (this->cpu->instruction_pointer() == *it ? "@" : " ") << " 0x" << hex << setfill('0') << setw(ADDRESS_WIDTH) << *it;
		#ifdef OUTPUTINSTRUCTIONS
		os << " : ";
		this->cpu->write_instruction(this->cpu->memory(*it));
		#else
		cout << endl;
		#endif
	}
}


void Shell::clone(vector<string>* args)
{
	if(args->size() != 1)
	{
		cout << "usage: clone <newName>" << endl;
		cout << "\t<newName>: mandatory parameter specifying the name of the cloned shell" << endl;
	}
	else
	{
		vector<string>::const_iterator argv = args->begin();
		Shell* clonedShell = new Shell(this->cpu->clone(), new ExecutionContext(*this->context), *argv);

		// suspend self
		this->state = Suspended;

		throw clonedShell;
	}
}


string Shell::get_name()
{
	return this->name;
}


ShellStates Shell::run()
{
	void (Shell::* action)(vector<string>*) = &Shell::nop;
	string command;
	vector<string> arguments;
	map < string, pair < void(Shell::*)(vector<string>*), string > >::const_iterator commandEntry;
	do
	{
		this->state = AwaitInput;

		vector<string> parsed_arguments;
		command = this->get_parsed_command(&parsed_arguments);
		if(command.length() > 0)
		{
			arguments = parsed_arguments;

			commandEntry = this->commandMap.find(command);
			if(commandEntry != this->commandMap.end())
			{
				action = commandEntry->second.first;
			}
			else
			{
				action = &Shell::unknown_command;
				arguments.insert(arguments.begin(), command);
			}

			parsed_arguments.clear();
		}

		// if an empty command was given; then re-use the old one

		this->state = Running;
		(this->*action)(&arguments);
	}
	while(this->state != Suspended
		&& this->state != Terminated);

	return this->state;
}


void Shell::run_program(vector<string>* args)
{
	this->context->run(this->cpu);
}


string Shell::get_parsed_command(vector<string>* args)
{
	string result;

	cout << this->name << " $> ";
	string fullCommand;
	getline(cin, fullCommand);

	// parse the command
	string::size_type spacePos = fullCommand.find(" ");
	result = fullCommand.substr(0, (spacePos != string::npos ? spacePos : fullCommand.length()));

	// get the arguments
	if(spacePos != string::npos)
	{
		string::size_type argStart = -1;
		bool inQuotes = false;
		char current = '\0';
		char previous = '\0';
		for(string::size_type index = spacePos + 1; index < fullCommand.length(); index++)
		{
			current = fullCommand[index];
			if(current == '"')
			{
				if(inQuotes)
				{
					// argStart == -1
					assert((signed int)argStart != -1);
					args->insert(args->end(), fullCommand.substr(argStart, index - argStart));

					// we exit the argument
					argStart = -1;
				}
				else
				{
					// start the argument here
					argStart = index + 1;
				}

				inQuotes = !inQuotes;
			}
			else if(iswspace(current))
			{
				// if we are inside quotes; ignore this whole thing
				if(!inQuotes)
				{
					// we see a space which means the previous argument was terminated
					// except if the previous was either a whitespace or a "
					// in which case we haven't started the argument yet
					if(!iswspace(previous)
						&& previous != '"')
					{
						// add the argument (from argStart until index -1)
						// argStart == -1
						assert((signed int)argStart != -1);
						args->insert(args->end(), fullCommand.substr(argStart, index - argStart));
					}

					// exit argument
					argStart = -1;
				}
			}
			else
			{
				// if we see anything but whitespace or quotes and we don't know
				// where the argument starts yet; then start the argument
				if(!inQuotes && (signed int)argStart == -1)
				{
					argStart = index;
				}
			}

			previous = current;
		}

		// if there is a lingering argStart; then this just means that the argument wasn't
		// terminated with a space or a "; so let's do that for you
		if((signed int)argStart != -1)
		{
			args->insert(args->end(), fullCommand.substr(argStart, fullCommand.length() - argStart));
		}
	}

	// normalize the command to lowercase
	int length = result.length();
	char* resultValue = new char[length + 1];
	const char* src = result.c_str();
	for(int n = 0; n < length; n++)
	{
		*(resultValue + n) = tolower(*(src + n));
	}

	// zero terminate
	*(resultValue + length) = 0;

	return string(resultValue);
}


void Shell::initialize_command_map()
{
	this->commandMap["breakpoint"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::breakpoint, "Shows or manipulates breakpoints");
	this->commandMap["clear"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::clear, "Clears the console");
	this->commandMap["clone"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::clone, "Clones the shell into a new named console");
	this->commandMap["cls"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::clear, "Clears the console");
	this->commandMap["dumpcore"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::dump_core, "Shows the contents of the CPU");
	this->commandMap["exit"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::exit, "Exits this shell (terminates)");
	this->commandMap["help"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::help, "Shows this help");
	this->commandMap["listshells"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::list_shells, "Shows all existing shells");
	this->commandMap["run"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::run_program, "Runs the program");
	this->commandMap["setip"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::set_instruction_pointer, "Sets the instruction pointer address");
	this->commandMap["shownext"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::show_next_instruction, "Shows the next instruction information");
	this->commandMap["showticks"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::show_ticks, "Shows the number of ticks executed by the CPU");
	this->commandMap["snapshot"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::snapshot_cpu, "Loads or saves a CPU snapshot");
	this->commandMap["step"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::step, "Executes the next instruction");
	this->commandMap["switch"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::switch_shells, "Suspends this shell and switches to another shell");
	this->commandMap["test"] = pair<void(Shell::*)(vector<string>*), string>(&Shell::test, "Executes the test method");
}


void Shell::nop(vector<string>* args)
{
}


void Shell::unknown_command(vector<string>* args)
{
	cout << "ERROR: \"" << args->front() << "\" is not a known command" << endl;
}


ShellStates Shell::get_state() const
{
	return this->state;
}


void Shell::set_instruction_pointer(vector<string>* args)
{
	if(args->size() == 1)
	{
		const char* const rawIp = args->begin()->c_str();
		unsigned int ip;
		int result = xtoi(rawIp, &ip);
		if(result == 0)
		{
			this->cpu->instruction_pointer(ip);
		}
		else
		{
			cout << "ERROR: The address '" << *(args->begin()) << "' could not be parsed" << endl;
		}
	}
	else
	{
		cout << "usage: setip <address>" << endl;
		cout << "<address>: the address to set the Instruction Pointer to" << endl;
	}
}


void Shell::exit(vector<string>* args)
{
	this->state = Terminated;
}


void Shell::list_shells(vector<string>* args)
{
	this->state = Suspended;

	ListShellsRequest request;
	throw request;
}


void Shell::switch_shells(vector<string>* args)
{
	if(args->size() == 1)
	{
		ShellSwitchRequest request;
		request.switch_to = *args->begin();

		// suspend self
		this->state = Suspended;

		throw request;
	}
	else
	{
		cout << "usage: switch <shellName>" << endl;
		cout << "<shellName>: name of an existing shell to switch to" << endl;
	}
}


void Shell::help(vector<string>* args)
{
	cout << "The following commands are available:" << endl;
	for(map < string, pair<void(Shell::*)(vector<string>*), string > >::iterator it = this->commandMap.begin(); it != this->commandMap.end(); it++)
	{
		cout << "\t" << it->first << ": " << it->second.second << endl;
	}
}


void Shell::dump_core(vector<string>* args)
{
	vector<string>::size_type argc = args->size();
	switch(argc)
	{
		case 0:
			this->cpu->dump_core(&cout);
			break;

		case 1:
		{
			vector<string>::const_iterator path = args->begin();
			cout << "Dumping core to '" << *path << "'...";
			ofstream outputStream;
			outputStream.open((*path).c_str(), ios::out | ios::trunc);
			if(outputStream.is_open())
			{
				outputStream << "Shell \"" << this->name << "\"" << endl;

				this->cpu->dump_core(&outputStream);
				outputStream.close();
				cout << "OK";
			}
			else
			{
				cout << "FAILED: could not open file";
			}

			cout << endl;
			break;
		}

		default:
			cout << "usage: dumpcore [filename]" << endl;
			cout << "\tfilename: optional parameter for the file to which to perform the core dump" << endl;
			cout << "\t          if not specified, the core dump is directed to the console" << endl;
			break;
	}
}


void Shell::snapshot_cpu(vector<string>* args)
{
	bool showHelp = false;

	if(args->size() == 2)
	{
		vector<string>::const_iterator item = args->begin();
		const char* pItem = (*item).c_str();

		if(strcmpci(pItem, "load") == 0)
		{
			item++;
			this->load_cpu_snapshot(*item);
		}
		else if(strcmpci(pItem, "save") == 0)
		{
			item++;
			this->save_cpu_snapshot(*item);
		}
		else
		{
			showHelp = true;
		}
	}
	else
	{
		showHelp = true;
	}

	if(showHelp)
	{
		cout << "usage: snapshot {load | save} <filename>" << endl;
		cout << "\tload:       Load the CPU state from the snapshot file <filename>" << endl;
		cout << "\tsave:       Save the CPU state from the snapshot file <filename>" << endl;
		cout << "\t<filename>: Name of the snapshot file" << endl;
	}
}


void Shell::load_cpu_snapshot(const string& filename)
{
	cout << "Loading CPU snapshot from '" << filename << "'...";
	ifstream snapshotStream;
	snapshotStream.open(filename.c_str(), (ios_base::openmode)(ios_base::binary | ios_base::in | ios_base::beg));
	if(snapshotStream.is_open())
	{
		this->cpu->load_memory(&snapshotStream);
		snapshotStream.close();

		cout << "OK";
	}
	else
	{
		cout << "FAILED: could not open file";
	}

	cout << endl;
}


void Shell::save_cpu_snapshot(const string& filename)
{
	cout << "Saving CPU snapshot into '" << filename << "'...";
	ofstream snapshotStream;
	snapshotStream.open(filename.c_str(), ios::binary | ios::out | ios::trunc);
	if(snapshotStream.is_open())
	{
		this->cpu->store_memory(&snapshotStream);
		snapshotStream.close();

		cout << "OK";
	}
	else
	{
		cout << "FAILED: could not open file";
	}

	cout << endl;
}


void Shell::clear(vector<string>* args)
{
	system("clear");
}


void Shell::show_next_instruction(vector<string>* args)
{
	bool parsed = false;
	unsigned int address = this->cpu->instruction_pointer();
	unsigned int count = 10;	 // TODO, thomasde: pull this from the context

	switch (args->size())
	{
		case 0:
			parsed = true;
			break;

		case 1:
			// if there is an '@' in there, then we need to split the count & address
			string arg = args->front();
			size_t ix = arg.find('@');

			if (ix == (size_t)-1)
			{
				// can only be "count"; so address remains IP and count = atoi(count)
				count = atoi(arg.c_str());
				parsed = count > 0;
			}
			else
			{
				// can be "count@address", "count@", "@address", "@"
				string count_arg = arg.substr(0, ix);
				string addr_arg = arg.substr(ix + 1);

				if (count_arg.size() > 0)
				{
					parsed = (count = atoi(count_arg.c_str()));
				}
				else
				{
					parsed = true;
				}

				if (addr_arg.size() > 0)
				{
					parsed &= (xtoi(addr_arg.c_str(), &address) == 0);
				}
				else
				{
					parsed &= true;
				}
			}

			break;
	}

	if (parsed == true)
	{
		while (address < CPU::MEMORY_SIZE && count > 0)
		{
			cout << "\t" << (this->cpu->instruction_pointer() != address ? " " : ">");
			this->show_instruction(address);

			address++;
			count--;
		}
	}
	else
	{
		cout << "usage: shownext {[count] | [count]@[address]}" << endl;
		cout << "\tcount:      number of instructions to show" << endl;
		cout << "\taddress:    hex address of first instruction to show (IP by default)" << endl;
	}
}


void Shell::show_instruction(unsigned int address) const
{
	Word* instruction = this->cpu->memory(address);

	#ifdef OUTPUTINSTRUCTIONS
	this->cpu->write_instruction(instruction, address);
	#else
	cout << "0x" << hex << setfill('0') << setw(ADDRESS_WIDTH) << address << " (";
	instruction->write_word(&cout);
	cout << ")" << endl;
	#endif
}


void Shell::show_ticks(vector<string>* args)
{
	cout << "CPU Ticks: " << this->cpu->Ticks() << endl;
}


void Shell::step(vector<string>* args)
{
	this->context->step(this->cpu);
}


void Shell::test(vector<string>* args)
{
	cout << "Entering Test Procedure" << endl;

	this->cpu->instruction_pointer(0);

	unsigned char ldaInstr[5];
	ldaInstr[0] = 0;
	ldaInstr[1] = 3;
	ldaInstr[2] = 0;
	ldaInstr[3] = L0_R5;
	ldaInstr[4] = LDA;
	this->cpu->memory(0)->value(ldaInstr);

	unsigned char ldxInstr[5];
	ldxInstr[0] = 0;
	ldxInstr[1] = 4;
	ldxInstr[2] = 0;
	ldxInstr[3] = L0_R5;
	ldxInstr[4] = LDX;
	this->cpu->memory(1)->value(ldxInstr);

	unsigned char jmpInstr[5];
	jmpInstr[0] = 0;
	jmpInstr[1] = 5;
	jmpInstr[2] = 0;
	jmpInstr[3] = 0;
	jmpInstr[4] = JMP_JSJ_JOV_JNOV_JL_JE_JG_JGE_JNE_JLE;
	this->cpu->memory(2)->value(jmpInstr);

	unsigned char valueForA[5];
	valueForA[0] = 1;
	valueForA[1] = 2;
	valueForA[2] = 3;
	valueForA[3] = 4;
	valueForA[4] = 5;
	this->cpu->memory(3)->value(valueForA);

	unsigned char valueForX[5];
	valueForX[0] = 6;
	valueForX[1] = 7;
	valueForX[2] = 8;
	valueForX[3] = 9;
	valueForX[4] = 10;
	this->cpu->memory(4)->value(valueForX);

	unsigned char slaInstr[5];
	slaInstr[0] = 0;
	slaInstr[1] = 2;
	slaInstr[2] = 0;
	slaInstr[3] = 5;
	slaInstr[4] = SLA_SRA_SLAX_SRAX_SLC_SRC;
	this->cpu->memory(5)->value(slaInstr);

	unsigned char hltInstr[5];
	hltInstr[0] = 0;
	hltInstr[1] = 0;
	hltInstr[2] = 0;
	hltInstr[3] = 2;
	hltInstr[4] = NUM_CHAR_HLT;
	this->cpu->memory(10)->value(hltInstr);

	this->cpu->dump_core(&cout);

	cout << "Test Done" << endl;
}
